'''
Created on 26.02.2012

@author: myaut
'''

from salsa.service import Service, Client

from salsa.service.repo import Repository
from salsa.service.macrodb import MacrosDB

from salsa.util.rpc import RPCProtocol, RPCInfo, RPCError
from salsa.util.io import IOFeederReply
from salsa.util import LoggerSeverity

from salsa import SALSAException

from salsa import do_trace

import sqlite3
import threading
import os
import sys

class SourceTreeException(SALSAException):
    pass

stree_db_create = ('''
    create table if not exists sources (
        id integer primary key autoincrement,
        
        name text,
        state integer,
        failmsg text
    );
''',
'''create table if not exists src_files (
        id integer primary key autoincrement,
        
        sid integer,
        state integer,
        hash text
    );
''','''
    create table if not exists src_deps (
        id integer primary key autoincrement,
        
        sid integer,
        did integer
    );
''')

class DependencyLoop:
    def __init__(self, src_fn):
        self.loop_files = [src_fn]
        
    def append(self, src_fn):
        self.loop_files.index(0, src_fn)
    
    def __str__(self):
        return '->'.join(self.loop_files)

class SourceState:
    S_FAILED = -1
    
    S_ADDED = 0
    S_PREPROC = 1
    S_PARSED = 2
    S_POSTPROC = 3
    
    def __init__(self, ss):
        ss = int(ss)
        
        if -1 <= ss < 3:
            self.ss = ss
        else:
            self.ss = 4
    
    def __str__(self):
        return ['FAILED', 'ADDED', 'PREPROC', 'PARSED', 'POSTPROC'][self.ss + 1]   

class Walker:
    def __init__(self, path, bl_fn, logger):
        self.path = path
        
        self.logger = logger
        
        self.blacklist = []
        if bl_fn:
            bl_file = file(bl_fn, 'r')
            blacklist = map(lambda bl_entry: bl_entry.strip(), bl_file.readlines())
            
            self.add_blacklist(blacklist)
        
        #initial values
        self.walk_stack = []
        self.dirlist = [os.listdir(self.path)]
        self.curpath = self.path

    def add_blacklist(self, bl):
        for bl_entry in bl:
            self.logger.log(LoggerSeverity.LS_DEBUG, "Added blacklist entry %s" % bl_entry)
        
        self.blacklist.extend(bl)
    
    def is_blacklisted(self, abs_fn):
        for bl_entry in self.blacklist:
            if abs_fn == bl_entry:
                self.logger.log(LoggerSeverity.LS_INFO, 'Ignoring path %s because it is in blacklist' % abs_fn)
            
                return True
        
        if os.path.basename(abs_fn).startswith('.'):
            self.logger.log(LoggerSeverity.LS_INFO, 'Ignoring path %s because it is hidden' % abs_fn)
        
            return True
        
        return False
    
    def fn_filter(self, fn):
        file_ext = fn.split('.')[-1]
        
        if file_ext == 'c' or file_ext == 'h':
            return True
            
        return False
    
    def go_back(self):
        while len(self.dirlist[-1]) == 0:
            # Root directory exhausted - finish
            if len(self.walk_stack) == 0:
                raise StopIteration()
            
            # No items are remaining - go back
            self.dirlist.pop()
            self.walk_stack.pop()
            
            self.curpath = os.path.join(self.path, '/'.join(self.walk_stack))

    def go_next(self):
        self.go_back()
        
        fn = self.dirlist[-1].pop()
        abs_fn = os.path.join('/'.join(self.walk_stack), fn)
        
        self.logger.log(LoggerSeverity.LS_DEBUG, 'Found path %s' % abs_fn)
        
        if self.is_blacklisted(abs_fn):
            return None
        
        if os.path.isdir(os.path.join(self.path, abs_fn)):
            # Add a level to hierarchy
            self.curpath = os.path.join(self.curpath, fn)
            self.walk_stack.append(fn)
            self.dirlist.append(os.listdir(self.curpath))
            
            # Directory is ignored
            return None
        
        elif not self.fn_filter(fn):
            self.logger.log(LoggerSeverity.LS_INFO, 'Ignoring path %s because it is filtered' % abs_fn)
            
            return None
       
        return abs_fn
    
    def absolute_fn(self, filename):
        return os.path.join(self.path, filename)
    
    def __iter__(self):
        return self
    
    def next(self):
        fn = None
        
        while not fn:            
            fn = self.go_next()
            
        return fn

# Adaptive Replacement Cache.
# Needed for fast search for sources without 
# querying database
class SourceNameDict(dict):
    min_size = 768
    max_size = 1024
    
    class Item:
        sid = 0
        
        uses = 1
        txid = 0
    
    def __init__(self, mapper):
        self.mapper = mapper
        self.txid = 0
        
        self.cache = {}
    
    def evict(self):
        def item_range(i1, i2):
            txid_diff = float((self.txid - i1.txid) - (self.txid - i2.txid)) 
            uses_diff = float(i1.uses - i2.uses)
            
            rate = int(txid_diff / float(self.max_size) + uses_diff)
            
            return -rate
        
        new_cache = sorted(self.cache.items(), cmp = lambda di1, di2: item_range(di1[1], di2[1]))
        
        self.cache = dict(new_cache[:self.min_size])
    
    def cache_item(self, name, item):
        if len(self.cache) >= self.max_size:
            self.evict()
        
        self.cache[name] = item
    
    def __getitem__(self, name):
        self.txid += 1
        
        if name in self.cache:
            item = self.cache[name]
            
            item.uses += 1
            return item.sid
        
        item = SourceNameDict.Item()
        item.sid = self.mapper(name)
        item.txid = self.txid
    
        self.cache_item(name, item)
        
        return item.sid
    
    def delete(self, name):
        if name in self.cache:
            del self.cache[name]
    
class SourceTree(Service):
    def __init__(self):
        Service.__init__(self)
        
        self.src_path = self.params['source_path']
        self.bl_fn = self.params.get('blacklist')
        
        self.sources = SourceNameDict(self.source_name_mapper)
        self.dep_paths = self.params['dep_paths'].split(',') 
        
        self.db_path = self.core.abs_data_path(self.params.get('db_path', 'stree.sqldb'))
        
        self.db_connect()
    
    def db_connect(self):
        self.db_conn = sqlite3.connect(self.db_path, check_same_thread = False)
            
        for db_create_expr in stree_db_create:
            self.db_conn.execute(db_create_expr)
        
        self.db_conn.commit()
        self.db_conn.text_factory = str
        
        self.db_lock = threading.Lock()
        
        self.logger.log(LoggerSeverity.LS_INFO, 'Source tree is connected to %s' % self.db_path)
    
    def add_source(self, name):
        # do_trace('Name: %s %s' % (repr(name), type(name)))
        # do_trace('State: %s %s' % (repr(SourceState.S_ADDED),type( SourceState.S_ADDED)))
        
        with self.db_lock:
            self.db_conn.execute("INSERT INTO sources VALUES (NULL, ?, ?, ?)", (name, SourceState.S_ADDED, ''))
        
        self.logger.log(LoggerSeverity.LS_NOTICE, 'Source file %s was added' % name)
    
    def add_source_ext(self, name):
        src_file_path = os.path.join(self.src_path, name)
        
        if not os.access(src_file_path, os.R_OK):
            raise SourceTreeException("Coundn't find file %s" % src_file_path)
        
        self.add_source(name)
        
        return RPCProtocol.STATUS_OK
    
    def delete_source(self, name):
        self.rollback_source(name)
        
        did = self.sources[name]
        
        self.db_conn.execute('DELETE FROM sources WHERE name = ?', (name, ))
        self.db_conn.execute('DELETE FROM src_deps WHERE did = ?', (did, ))
        
        self.db_conn.commit()
        
        self.logger.log(LoggerSeverity.LS_NOTICE, 'Deleted source %s' % name)
        
        self.sources.delete(name)
        
        return RPCProtocol.STATUS_OK
    
    def remove_source_files(self, sid):
        cur_files = self.db_conn.execute('SELECT state, hash FROM src_files WHERE sid = ?', (sid, ))
        files = cur_files.fetchall()
    
        repo = Client('svc.repo', Repository)
        
        try:
            for (state, hash) in files:
                print >> sys.stderr, hash
                
                repo.destroy_file(hash)
        finally:
            repo.stop()
    
    def remove_macroses(self, src_name):
        macrosdb = Client('svc.macrosdb', MacrosDB)
        
        macrosdb.delete_source(src_name)        
        macrosdb.stop()
    
    def rollback_source(self, src_name):
        sid = self.sources[src_name]
        
        # Remove all files and macroses
        self.remove_source_files(sid)
        self.remove_macroses(src_name)
        
        with self.db_lock:
            self.db_conn.execute('UPDATE sources SET state = ?, failmsg = ? WHERE id = ?', (SourceState.S_ADDED, '', sid))
            
            self.db_conn.execute('DELETE FROM src_deps WHERE sid = ?', (sid, ))
            self.db_conn.execute('DELETE FROM src_files WHERE sid = ?', (sid, ))
            
            self.db_conn.commit()
        
        self.logger.log(LoggerSeverity.LS_NOTICE, 'Rolled back source %s' % src_name)
        
        return RPCProtocol.STATUS_OK
    
    def source_info(self, src_name):
        sid = self.sources[src_name]
        
        with self.db_lock:
            cur_info =  self.db_conn.execute('SELECT state, failmsg FROM sources WHERE id = ?', (sid, ))
            state, failmsg = cur_info.fetchone()
            
            cur_deps = self.db_conn.execute('SELECT s.name, s.state FROM sources s LEFT JOIN src_deps d ON d.did = s.id WHERE d.sid = ?', (sid, ))
            deps = cur_deps.fetchall()
            
            cur_files = self.db_conn.execute('SELECT state, hash FROM src_files WHERE sid = ?', (sid, ))
            files = cur_files.fetchall()
        
        return {'state' : state, 'failmsg': failmsg, 'deps': deps, 'files': files}
    
    def add_source_dep(self, sid, src_fn, dep):
        for dep_path in self.dep_paths:
            resolved_dep = os.path.join(dep_path, dep)
            
            try:
                did = self.sources[resolved_dep]
                    
                with self.db_lock:
                    self.db_conn.execute("INSERT INTO src_deps VALUES (NULL, ?, ?)", (sid, did))
            except KeyError:
                # Dependency failed to resolve for whis path, ignore
                continue
            else:
                # Dependency successfully resolved, exit loop
                break
        else:
            self.logger.log(LoggerSeverity.LS_WARNING, "Dependency %s couldn't be resolved. Ignoring" % (dep))
    
    def advance_source(self, sid, newstate, filehash):
        with self.db_lock:
            self.db_conn.execute('UPDATE sources SET state = ? WHERE id = ?', (newstate, sid))
            self.db_conn.execute('INSERT INTO src_files VALUES (NULL, ?, ?, ?)', (sid, newstate, filehash))
    
    def mark_failed_source(self, src_fn, failmsg):
        sid = self.sources[src_fn]
        
        with self.db_lock:
            self.db_conn.execute('UPDATE sources SET state = ?, failmsg = ? WHERE id = ?', (SourceState.S_FAILED, failmsg, sid))
            self.db_conn.commit()
        
        return RPCProtocol.STATUS_OK
    
    def source_name_mapper(self, name):
        with self.db_lock:
            cursor = self.db_conn.execute('SELECT id FROM sources WHERE name = ?', (name, ))
            result = cursor.fetchone()
        
        if not result:
            raise KeyError(name)
        
        return result[0]
    
    def abs_source_path(self, path):
        return os.path.join(self.src_path, path)
    
    def init_tree(self):        
        self.walker = Walker(self.src_path, self.bl_fn, self.logger)
        
        for fn in self.walker:
            self.add_source(fn)
        
        self.db_conn.commit()
        
        return RPCProtocol.STATUS_OK
    
    def _walk_tree(self, fieldlist, sstate, sstate_op, deps_for):        
        where_clauses = []
        where_args = []
        left_join = ''
        
        # Process 
        if sstate != None:
            ss = SourceState(sstate)
            
            where_clauses.append('state %s ?' % sstate_op)
            where_args.append(ss.ss)
        
        if deps_for:
            sid = self.sources[deps_for]
            
            deps_select = 'select d.did from sources s, src_deps d where d.sid = ?' 
            where_clauses.append('id in (%s)' % deps_select)
            where_args.append(sid)
            
            #where_clauses.append('d.sid = ?')
            #left_join = 'LEFT JOIN src_deps d ON sources.id = d.did'
        
        query = 'SELECT %s FROM sources %s' % (', '.join(fieldlist), left_join)
        
        if where_clauses:
            query += ' WHERE ' + ' AND '.join(where_clauses)
            
        with self.db_lock:
            cursor = self.db_conn.execute(query, tuple(where_args))
                
        return cursor
    
    def get_deps(self, srcid):
        cursor = self.db_conn.execute('select d.did from src_deps d where d.sid=?', (srcid, ))
        deps = cursor.fetchall()
        
        return deps
    
    def list_tree(self, fieldlist = ['*'], sstate = None, sstate_op = '=', deps_for = None):
        cursor = self._walk_tree(fieldlist, sstate, sstate_op, deps_for)
        tree = cursor.fetchall()
        
        return tree
    
    def walk_tree(self, fieldlist = ['*'], sstate = None, sstate_op = '=', deps_for = None):
        tree = self.list_tree(fieldlist, sstate, sstate_op, deps_for)
        
        return IOFeederReply(iter(tree))
    
    def commit_preproc(self, src_name, pp_file, deps):
        sid = self.sources[src_name]
        
        self.advance_source(sid, SourceState.S_PREPROC, pp_file)
        
        for dep in deps:
            self.add_source_dep(sid, src_name, dep)
            
        self.db_conn.commit()
        
        return RPCProtocol.STATUS_OK
    
    def commit_parsed(self, src_name, ast_file):
        sid = self.sources[src_name]
        
        self.advance_source(sid, SourceState.S_PARSED, ast_file)
        
        self.db_conn.commit()
        
        return RPCProtocol.STATUS_OK
    
    def get_file_hash(self, src_name, state):
        sid = self.sources[src_name]
        
        with self.db_lock:
            cursor = self.db_conn.execute('SELECT hash FROM src_files WHERE sid = ? AND state = ?', (sid, state))
        
        return cursor.fetchone()[0]
    
    def do_stop(self):
        self.db_conn.close()
        
SourceTree.rpc_info = RPCInfo()
SourceTree.rpc_info.register(SourceTree, 'abs_source_path', 'init_tree', 'get_file_hash',
                                'walk_tree', 'commit_preproc', 'commit_parsed',
                                'rollback_source', 'source_info', 'mark_failed_source',
                                'get_deps', 'delete_source', 'add_source_ext')